Record projection with another record projection - java

I get problem with nessted projection. (inside projection)
Root entity:
#Entity(name = "AAA")
#NoArgsConstructor
#AllArgsConstructor
#Data
public class AAA{
#Id
private Long id;
#OneToOne
private BBB bbb;
}
where BBB looks like this:
#Entity(name = "BBB")
#NoArgsConstructor
#AllArgsConstructor
#Data
public class BBB{
#Id
private Long id;
#Column(name = "name")
private String name;
Projections
public record AAAProjection(
Long id,
BBBProjection bbb
) {
}
public record BBBProjection(
Long id,
String name
) {
}
When I try to query with these projections, an exception is thrown:
org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class AAAProjection
Is there any way to use nested projection in projection in Spring Boot Data JPA?

I solved this using traversing in projection, like this:
public record AAAProjection(
Long id,
Long bbbId,
String bbbName
) {
}
To get a field from a nested entity just use its name in the projection, here is BBBProjection bbb so there is the pattern like:
bbb<FieldName>

Related

How to use "select new Dto(...)..." construction with #OneToMany relationship in Spring Data JPA #Query?

I need to use "select new ..." to construct DTO object in my repository.
I have following classes:
Entity class Victim:
#Entity
#Table(name = "victims")
#Getter
#Setter
#ToString
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Victim extends User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 13)
private String phoneNumber;
#Enumerated(EnumType.STRING)
#Column(nullable = false)
private UserStatus userStatus = UserStatus.ACTIVE;
#JsonManagedReference
#Setter(AccessLevel.PRIVATE)
#BatchSize(size = 100)
#OneToMany(mappedBy = "victim", cascade = CascadeType.ALL)
#Exclude
private List<Request> requests = new ArrayList<>();
#JsonManagedReference
#OneToOne(mappedBy = "victim", cascade = CascadeType.ALL)
private LegalEntityVictim legalEntityVictim;
#JsonManagedReference
#OneToOne(mappedBy = "victim", cascade = CascadeType.ALL)
private NaturalPersonVictim naturalPersonVictim;
}
Also assume, that Request is the object, which has its own relationship with another object and contains it as well - probably that is the problem, because without using that field in DTO everything works - so the possible question is: “How to construct DTO with inner one-to-many dependency (which could have its own dependencies as well)?”
DTO class VictimDTO:
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#ToString
public class VictimDto {
private Long id;
private String email;
private String password;
private String name;
private String surname;
private String nameOfOrganization;
private String phoneNumber;
private UserStatus userStatus;
private Collection<Request> requests = new ArrayList<>();
}
Repository VictimRepository:
public interface VictimRepository extends JpaRepository<Victim, Long> {
#Query("select new com.pavliuk.dto.VictimDto(v.id, v.email, v.password, v.naturalPersonVictim.name, v.naturalPersonVictim.surname, v.legalEntityVictim.nameOfOrganization, v.phoneNumber, v.userStatus, v.requests) from Victim v ")
Page<VictimDto> findAllDto(Pageable pageable);
}
The following hierarchy of SQL tables are defined:
And as result I got following logs (I have turned on show-sql ability, so you can inspect also generated query):
Hibernate: select victim0_.id as col_0_0_, victim0_.email as col_1_0_, victim0_.password as col_2_0_, naturalper1_.name as col_3_0_, naturalper1_.surname as col_4_0_, legalentit3_.name_of_organization as col_5_0_, victim0_.phone_number as col_6_0_, victim0_.user_status as col_7_0_, . as col_8_0_ from victims victim0_ cross join natural_person_victims naturalper1_ cross join legal_entity_victims legalentit3_ inner join requests requests4_ on victim0_.id=requests4_.victim_id where victim0_.id=naturalper1_.victim_id and victim0_.id=legalentit3_.victim_id limit ?
2023-01-04 08:54:22.282 WARN 37428 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 42601
2023-01-04 08:54:22.282 ERROR 37428 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: syntax error at or near "."
Position: 273
2023-01-04 08:54:22.293 ERROR 37428 --- [nio-8080-exec-1] c.p.controller.ErrorHandlingController : handleException: message: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet, method: getAll
That's not possible because DTO projections in JPQL are inherently scalar. 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(Victim.class)
public interface VictimDto {
#IdMapping
Long getId();
String getEmail();
String getPassword();
String getName();
String getSurname();
String getNameOfOrganization();
String getPhoneNumber();
UserStatus getUserStatus();
Collection<RequestDto> getRequests();
#EntityView(Request.class)
interface RequestDto {
#IdMapping
Long getId();
String getName();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
VictimDto a = entityViewManager.find(entityManager, VictimDto.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<VictimDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!

Join a many to many relation with QueryDSL and JPASQLQuery

I have the following entities:
#AllArgsConstructor
#EqualsAndHashCode(of = {"name"})
#Data
#NoArgsConstructor
#Entity
#Table(schema = "eat")
public class Pizza {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator="pizza_id_seq")
private Integer id;
#NotNull
private String name;
#NotNull
#Positive
private Double cost;
#ManyToMany
#JoinTable(schema = "eat",
name = "pizza_ingredient",
inverseJoinColumns = { #JoinColumn(name = "ingredient_id") })
private Set<Ingredient> ingredients;
}
#AllArgsConstructor
#EqualsAndHashCode(of = {"name"})
#Data
#NoArgsConstructor
#Entity
#Table(schema = "eat")
public class Ingredient {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator="ingredient_id_seq")
private Integer id;
#NotNull
#Size(min=1, max=64)
private String name;
}
I'm using JPASQLQuery object provided by QueryDSL (4.2.2) to create some native queries in PostgreSQL:
public JPASQLQuery<T> getJPASQLQuery() {
return new JPASQLQuery<>(
entityManager,
PostgreSQLTemplates.builder().printSchema().build()
);
}
The problem comes trying to use join functions, for example:
QIngredient ingredient = QIngredient.ingredient;
QPizza pizza = QPizza.pizza;
StringPath ingredientPath = Expressions.stringPath("ingredient");
StringPath pizzaPath = Expressions.stringPath("pizza");
NumberPath<Double> costPath = Expressions.numberPath(Double.class, "cost");
Expression rowNumber = SQLExpressions.rowNumber().over().partitionBy(ingredientPath).orderBy(costPath.desc()).as("rnk");
JPASQLQuery subQuery = getJPASQLQuery()
.select(ingredient.name.as(ingredientPath), pizza.name.as(pizzaPath), pizza.cost.as(costPath), rowNumber)
.from(pizza)
// The error is in next innerJoin
.innerJoin((SubQueryExpression<?>) pizza.ingredients, ingredient)
.where(ingredient.name.in(ingredientNames));
If I keep the current innerJoin((SubQueryExpression<?>) pizza.ingredients, ingredient) I receive:
class com.querydsl.core.types.dsl.SetPath cannot be cast to class com.querydsl.core.types.SubQueryExpression
I cannot remove current (SubQueryExpression<?>) because innerJoin doesn't accept SetPathas parameter.
On the other hand, the following:
.from(pizza)
.innerJoin(ingredient)
Doesn't work due to pizza_ingredient is not included in the generated query.
How can I use innerJoin in JPASQLQuery with a many to many relationship like above?
Basically, there are two main approaches trying to solve it:
Include required native functions
As suggest one QueryDSL developer here, replacing JPASQLQuery by JPA alternatives.
Create required Path for many to many table
First is important to add name property into every #Table annotation because internally is the one used by QueryDSL NativeSQLSerializer class to generate from and join clauses.
So, for example:
#Table(schema = "eat")
public class Pizza ...
Should be replaced by:
#Table(name = "pizza", schema = "eat")
public class Pizza ...
Next, create for custom Path for the many to many table:
RelationalPathBase<Object> pizzaIngredient = new RelationalPathBase<>(Object.class, "pi", "eat", "pizza_ingredient");
NumberPath<Integer> pizzaIngredient_PizzaId = Expressions.numberPath(Integer.class, pizzaIngredient, "pizza_id");
NumberPath<Integer> pizzaIngredient_IngredientId = Expressions.numberPath(Integer.class, pizzaIngredient, "ingredient_id");
So the complete code would be:
QIngredient ingredient = QIngredient.ingredient;
QPizza pizza = QPizza.pizza;
RelationalPathBase<Object> pizzaIngredient = new RelationalPathBase<>(Object.class, "pi", "eat", "pizza_ingredient");
NumberPath<Integer> pizzaIngredient_PizzaId = Expressions.numberPath(Integer.class, pizzaIngredient, "pizza_id");
NumberPath<Integer> pizzaIngredient_IngredientId = Expressions.numberPath(Integer.class, pizzaIngredient, "ingredient_id");
StringPath ingredientPath = Expressions.stringPath("ingredient");
StringPath pizzaPath = Expressions.stringPath( "pizza");
NumberPath<Double> costPath = Expressions.numberPath(Double.class, "cost");
Expression rowNumber = SQLExpressions.rowNumber().over().partitionBy(ingredientPath).orderBy(costPath.desc()).as("rnk");
NumberPath<Long> rnk = Expressions.numberPath(Long.class, "rnk");
SubQueryExpression subQuery = getJPASQLQuery()
.select(ingredient.name.as(ingredientPath), pizza.name.as(pizzaPath), pizza.cost.as(costPath), rowNumber)
.from(pizza)
.innerJoin(pizzaIngredient).on(pizzaIngredient_PizzaId.eq(pizza.id))
.innerJoin(ingredient).on(ingredient.id.eq(pizzaIngredient_IngredientId))
.where(ingredient.name.in(ingredientNames));
return getJPASQLQuery()
.select(ingredientPath, pizzaPath, costPath)
.from(
subQuery,
Expressions.stringPath("temp")
)
.where(rnk.eq(1l))
.fetch();

Map string properties to JSONB

I've been trying map my string properties to Postgresql's JSONB using JPA. I did read perfect article by Vlad Mihalcea many times and also seen relative questions and problems with similar stuff. BUT I still have this exception org.postgresql.util.PSQLException: ERROR: column "json_property" is of type jsonb but expression is of type character varying every time when I'm trying insert something into my table.
And what even worse is - all these advices in similar questions were useful until I changed my entity class and made him inherits super class. And now situation is like this:
If #TypeDef and #Type on my child class and it works great
But I want use abstraction layer and set annotations, which I noticed above, to my base entity class and after that exception says me 'Hello! It's me again'
My hierarchy is pretty simple and here it is:
Base entity
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
#MappedSuperclass
public abstract class AbstractServiceEntity implements Serializable {
private Integer id;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
Child entity
#Entity
#Table(schema = "ref", name = "test_json_3")
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
public class TestJson extends AbstractServiceEntity {
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
private String jsonProperty;
My table
create table ref.test_json_3
(
id serial primary key,
json_property jsonb
)
UPD
I've succesfully inserted record with JPA native query, but I had to unwrap my query into hibernate query. Not sure that it's the most convinient way to manage inserting data into DB. The my question is actual, I still need your help) Example with native query below.
Code snippent with result
#Repository
public class JpaTestRepository {
#PersistenceContext
private EntityManager entityManager;
#Transactional
public void insert(TestJson testJson) {
entityManager.createNativeQuery("INSERT INTO test_json_3 (json_property) VALUES (?)")
.unwrap(Query.class)
.setParameter(1, testJson.getJsonProperty(), JsonBinaryType.INSTANCE)
.executeUpdate();
}
Finally I found solution for my problem. Answer is - just use your #Column(columnDefinition = "jsonb") and #Type(type = "jsonb" via getters but not class properties.
entity definition
#Entity
#Table(schema = "ref", name = "test_json_3")
#NoArgsConstructor
#AllArgsConstructor
#Setter
public class TestJson extends AbstractServiceEntity {
private String jsonProperty;
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
public String getJsonProperty() {
return jsonProperty;
}
You can try to add #TypeDefs under class TestJson:
#TypeDefs({
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class TestJson extends AbstractServiceEntity {
Alternate solution for mapping String to Jsonb type. Just add the following annotation on your string.
#ColumnTransformer(write = "?::jsonb")
private String jsonProperty;

Spring JPA can't search for nested object despite cascade attribute set

so I have an object Product with, let's say, private String name and private Origin origin.
Code is as follows:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#Data
#NoArgsConstructor
#RequiredArgsConstructor
#AllArgsConstructor
public abstract class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#NonNull
private String name;
#NonNull
#OneToOne(cascade = CascadeType.ALL)
private Origin origin;
}
Now I'm using a data loader to add some products into the database on startup like this:
(Wine extends Products and adds a few more Strings)
repository.save(new Wine(
"Test wine"
new Origin(
"Crete",
"Greece"
)
));
Additionally, I have a search endpoint where I can search for either the name or the Origin.
public Page<Wine> searchProducts(
#RequestParam(name = "text", required = false) String searchTerm,
#RequestParam(required = false) Origin origin) {
return wineService.searchWines(
searchTerm,
origin
);
}
and in the wine Service, I create a query using specifications. Putting them all in here would be overkill so just take a look at this:
public static Specification<Wine> hasOrigin(Origin origin) {
return (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get(Wine_.ORIGIN), origin);
}
Strangely enough, it seemed to work some time ago but now it doesn't. Now, if I search for a Wine with a certain origin, it just says:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: [censored].backend.model.Origin
How do I fix this? I've seen like 5 different posts regarding this problem and ALL of them said "well, just add cascade ALL to the Object" but I've already done that from the beginning.
Edit:
Origin Entity code:
#Entity
#Data
#NoArgsConstructor
#RequiredArgsConstructor
#AllArgsConstructor
#Table(name = "regions")
public class Origin {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonIgnore
private long id;
#NonNull
private String region;
#NonNull
private String country;
}
First join with Origin then create two Predicate and add them in builder and return.
public static Specification<Wine> hasOrigin(Origin origin) {
return new Specification<Wine>() {
#Override
public Predicate toPredicate(Root<Wine> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
final List<Predicate> predicates = new ArrayList<>();
Join<Wine, Origin> originRoot = root.join("origin");
predicates.add(criteriaBuilder.equal(originRoot.get("country"), origin.getCountry()));
predicates.add(criteriaBuilder.equal(originRoot.get("region"), origin.getRegion()));
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}

Failed to convert from type [java.lang.String] to type [java.lang.Long] for Spring JPA?

I have a problem about implementing two entities named for Country and State in Spring JPA.
I wrote a code to pose Country and State entities shown below.
Country Entity
#Entity
#Table(name="COUNTRY",catalog ="SPRINGANGULAR")
#Data
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode(of = {"id","code","countryName"}, exclude = "states")
public class Country {
#Id
#SequenceGenerator(name="COUNTRY_SEQ", sequenceName="COUNTRY_SEQ", allocationSize=1)
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="COUNTRY_SEQ")
#Column(name="ID", nullable = false)
private long id;
#Column(name="CODE")
private String code;
#Column(name="COUNTRY_NAME")
private String countryName;
#OneToMany(fetch=FetchType.LAZY,mappedBy="country",
cascade= CascadeType.ALL)
private Set<State> states = new HashSet<State>();
public Country(String code, String countryName) {
super();
this.code = code;
this.countryName = countryName;
}
}
State Entity
#Entity
#Table(name="STATE",catalog ="SPRINGANGULAR")
#Data
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode(of = {"id", "stateName"})
public class State {
#Id
#SequenceGenerator(name="STATE_SEQ", sequenceName="STATE_SEQ", allocationSize=1)
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="STATE_SEQ")
#Column(name="ID", nullable = false)
private long id;
#Column(name="STATE_NAME")
private String stateName;
#ManyToOne(fetch=FetchType.LAZY,cascade= CascadeType.ALL)
#JoinColumn(name = "COUNTRY_ID", nullable = false)
private Country country;
public State(String stateName, Country country) {
super();
this.stateName = stateName;
this.country = country;
}
}
Then I create a State Reporistory to get states by country code.
#CrossOrigin("http://localhost:4200")
#RepositoryRestResource(collectionResourceRel = "states", path = "states")
public interface StateRepository extends JpaRepository<State, Long>{
#Query("SELECT s FROM State s \n" +
"LEFT JOIN Country c ON c.id = c.id \n" +
"WHERE c.code = :code \n" +
"ORDER BY s.id ASC")
List<State> findByCountryCode(#RequestParam("code") String code);
}
Then I write a connection url in Postman to check if it works.
http://localhost:8080/api/states/findByCountryCode?code=BR
It throws an error
Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'findByCountryCode'; nested exception is java.lang.NumberFormatException: For input string: "findByCountryCode"
How can I fix the issue?
The solution is shown below.
I wrongly use this url http://localhost:8080/api/states/findByCountryCode?code=BR
Convert url
http://localhost:8080/api/states/findByCountryCode?code=BR`
to
http://localhost:8080/api/states?code=BR
Looks to me like a problem with the REST controller.
I assume you have a mapping for /api/states/{id} somewhere, like this:
#RestController
#RequestMapping(path = "api/states/")
public class StateController {
#GetMapping("/{id}")
public State getState(#PathVariable("id") long id) {
...
}
}
When you call /api/states/findByCountryCode?code=BR, it then interprets findByCountryCode as path variable id of type long, which will result in exactly this exception:
NumberFormatException: For input string: "findByCountryCode"
Solution:
either restrict the id path parameter with a regex pattern (numeric), or
use a different path to query the states (such as /api/states?code=BR)
The latter pattern is a common standard for listing and finding resources:
no query parameters: list all
with query parameters: list matching -> find functionality
So something like this:
#GetMapping("/")
public List<State> find(#RequestParam(value = "code", required = false) String code) {
if (code != null) {
// find by code
}
else {
// list all
}
}
Your query is a mix of native SQL and JPQL, definitely not valid. Here is the valid one:
#Query("SELECT s FROM State s \n" +
"WHERE s.country.code = :code \n" +
"ORDER BY s.id ASC")
There is no need for explicit left join, it is done by s.country expression.
Also, there is no such thing as LEFT JOIN ON in JPQL. Here are some join examples
Also, the URL should be something like:
http://localhost:8080/states/search/findByCountryCode?code=BR
You are missing a search as path element.

Categories