Using Projecions to fetch a particular column from child table - java

I have two tables
Loan (id, amount, duration)
LoanStatus(id, status, loan_id) // just an example, but it has lot more fields in this table
Loan.java
public class Loan{
private int id;
private int amount;
private int duration;
private LoanStatus loanStatus;
// getter and setters
}
LoanStatus.java
public class LoanStatus{ // just an example, but it has many fields than what actually given
private int id;
private String status;
private Loan loan;
//getter and setters
}
Now I would like to get only amount , duration , loanStatus.status using Projections. I've used createAlias() to successfully fetch that particular column, but the problem occurs when setting it to a setter.
Criteria criteria = getSession().createCriteria(Loan.class,"loan");
criteria.createAlias("loan.loanStatus", "loanStatus")
.setProjection(Projections.projectionList().add(Projections.property("id"),"id")
.add(Projections.property("amount"),"amount")
.add(Projections.property("duration"),"duration")
.add(Projections.property("loanStatus.status"), "loanStatus"))
.add(Restrictions.eq("id", id))
.setResultTransformer(Transformers.aliasToBean(Loan.class));
return criteria.list();
I've an error which as follows.
IllegalArgumentException occurred while calling setter for property [com.site.dto.Loan.loanStatus (expected type = com.site.dto.LoanStatus)]; target = [com.site.dto.Loan#4083974a], property value = [Pending]
So I'm getting my expected column value "Pending", but the problem is when setting it to a setter. I've seen many question for projections, but most of them was based on Restrictions using Projections but not fetching a child's particular column using projections.

Write your own custom transformer. The following implementation might be exactly what you need
https://github.com/samiandoni/AliasToBeanNestedResultTransformer . The usage example as written in their docs
class Person {
private Long id;
private String name;
private Car car;
// getters and setters
}
class Car {
private Long id;
private String color;
// getters and setters
}
List<Person> getPeople() {
ProjectionList projections = Projections.projectionList()
.add(Projections.id().as("id"))
.add(Projections.property("name").as("name"))
.add(Projections.property("c.id").as("car.id"))
.add(Projections.property("c.color").as("car.color"));
Criteria criteria = getCurrentSession().createCriteria(Person.class)
.createAlias("car", "c")
.setProjection(projections)
.setResultTransformer(new AliasToBeanNestedResultTransformer(Person.class));
return (List<Person>) criteria.list();
}
// each car of Person will be populated

Related

How to convert nested Entity to DTO using MapStruct or Java Stream?

In my Spring Boot apps, I generally use projection in order to return joined table results. However, I am trying to use Java Stream to map and return nested entities to the corresponding DTOs. Here is an example that I generally encountered:
Note: I setup entity relationship using Hibernate and that part is completely ok. For this reason, I omitted the related code for brevity. Just concentrate on mapping entities to the nested DTOs.
Country has many States, States has many Towns...
Country:
public class Country {
private Long id;
private String name;
#OneToMany(...)
private Set<State> states = new HashSet<>();
}
State:
public class State {
private Long id;
private Long population;
#ManyToOne(...)
private Country country;
#OneToMany(...)
private Set<Town> towns = new HashSet<>();
}
Town:
public class Town {
private Long id;
private String name;
#ManyToOne()
private State state;
}
I want to get Country list with Country Name, Sum of state population belonging to this country and Town list belonging to the country.
For this purpose, I created the following DTOs, but not sure if it is ok and how can I map the necessary data to this DTO in one step (I don't want to go to database 3 times, instead, I just want to map the country list to the corresponding fields (the list has all of these data as I built the relations properly using Hibernate).
#Data
public class CountryResponse {
private Long id;
private String name;
private Set<StateResponse> states;
private Long population;
private Set<TownResponse> towns;
public CountryResponse(Country country) {
this.id = country.getId();
this.name = country.getName();
// ???
this.states = country.getStates.stream().map(StateResponse::new)
.collect(Collectors.toSet());
this.towns = this.states.stream().map(TownResponse::new)
.collect(Collectors.toSet());
}
}
How can I do this? I would also consider using MapStruct if it is better for this scenario?
To get the sum of state populations, you can use the following stream code
country.getStates().stream().map(State::getPopulation).sum();
and the list of towns can be fetched as follows
country.getStates().stream().map(State::getTowns).flatMap(Set::stream).collect(Collectors.toList());

Projections on nested collections

I have three entities Topic, Subject and Category. How can I prefetch id and name columns for each of the entity while retrieving all the categories with subjects and topics? I don't need other fields since it affects the performance.
#Entity
class Topic{
private Long id;
private String name;
...
//other fields
}
#Entity
class Subject{
private Long id;
private String name;
...
//other fields
#OneToMany(fetch=FetchType.LAZY)
private List<Topic> topics;
}
#Entity
class Category{
private Long id;
private String name;
...
//other fields
#OneToMany(fetch=FetchType.LAZY)
private List<Subject> subjects;
}
I would suggest creating a result class on top of the projection itself as it eases the result set handling. The ResultClass needs to have a constructor with relevant query result fields and you have to use the fully qualified name in the query itself.
select new org.mypkg.ResultClass(c.id, c.name, s.id, s.name, t.id, t.name)
from Category c
inner join c.subjects s
inner join s.topics
Then you simply:
List<ResultClass> results = em.createQuery(query).list();

Hibernate mapping of internationalized database

I want to get international content from database based on locale provided in hibernate query. This is a question about hibernate mapping but please feel free to propose better database design if mine is wrong.
My DB design (simplified):
db design
So I have table with non translatable data and additional table with translated content but with additional field "locale" for distinction of language.
My java classes looks like this:
public class Car {
private Long id;
private Long length;
private Long weight;
private CarTranslated carTranslated;
// getters and setters
public class CarTranslated {
private Long id;
private Long carId;
private String desc;
// getters and setters
I want to be able to get one car with single query. With regular jdbc I would use something like this sql query:
public Car getById(Long id, Locale locale) {
Car c = new Car();
String sql = "select c.car_id, c.length, c.weight, ct.id, ct.descryption,
ct.car_id as "Translated car_id" from car c join car_translated ct on
(c.car_id = ct.car_id) where c.car_id ="+ id+" and ct.locale ='"+locale+"'";
// code to set fields of the object using ResultSet
return c;
}
What would be a hibernate annotation mapping and query for this setup? I tried several attempts but to no avail. Currently my best attempt was as below:
Mapping:
#Entity
#Table(name="CAR")
public class Car {
#Id
#Column(name="car_id")
private Long carId;
#Column (name="weight")
private Long carWeight;
#Column (name="length")
private Long carLength;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name ="CAR_ID")
private CarTranslated localized;
// getters and setters
#Entity
#Table(name="CAR_TRANSLATED")
public class CarTranslated {
#Id
#Column (name="id")
private Long id;
#Column (name="car_id")
private Long carId;
#Column (name="descryption")
private String desc;
#Column(name="locale")
private Locale locale;
DAO:
public Car getCarById(Locale locale, Long id) {
Car car = new Car();
try {
Session session = HibernateUtils.getSessionFactory().openSession();
Criteria cr = session.createCriteria(Car.class)
.add(Restrictions.eq("carId", id));
Criteria cr1 = session.createCriteria(CarTranslated.class)
.add(Restrictions.eq("locale", locale));
car = (Car) cr.uniqueResult();
car.setLocalized((CarTranslated) cr1.uniqueResult());
} catch (Exception e) {
System.out.println(e.getMessage());
}
return car;
}
This is a work-around and I'm wondering what would be a proper way to do this?
You should have an annotation on both columns when mapping to a FK. (JavaDoc)

JPA CriteriaQuery join three tables not directly navigable

i need to translate this sql query to jpa criteria:
SELECT tbl1.id_t1, tbl2.name, tbl3.name, tbl4.symbol, tbl1.limit, tbl1.value, tbl1.uncertainty
FROM table_1 AS tbl1
JOIN table_2 AS tbl2 ON tbl2.id_t2=tbl1.id_t2
JOIN table_3 AS tbl3 ON tbl3.id_t3=tbl1.id_t3
JOIN table_4 AS tbl4 ON tbl4.id_t4=tbl1.id_t4
WHERE (tbl2.id_l=1 AND tbl3.id_l=1) AND tbl1.id_s=1;
my mapping between pojo and database table are as follows:
Table_1
#Entity
#Table("table_1")
public class Table1 {
#Id
#Column(name="id_t1")
private Long idRowT1
#ManyToOne
#JoinColumn(name="id_t2")
private Table2 tbl2;
#ManyToOne
#JoinColumn(name="id_t3")
private Table3 tbl3;
#ManyToOne
#JoinColumn(name="id_t4")
private Table4 tbl4;
#Column(name="limit")
private String limit;
#Column(name="value")
private String value;
#Column(name="uncertainty")
private String uncertainty;
// getter and setter
}
Table_2
#Entity
#Table("table_2")
public class Table2 {
#Id
#Column(name="id_t2")
private Long idT2;
// getter and setter
}
Table_2_lang
#Entity
#Table("table_2_lang")
#IdClass(Table2LangPK.class)
public class Table2Lang {
#Id
#Column(name="id_t2")
private Long idT2;
#Id
#Column(name="id_l")
private Lang l;
#Column(name="name")
private String name;
// getter and setter
}
Table_3
#Entity
#Table("table_3")
public class Table3 {
#Id
#Column(name="id_t3")
private Long idT3;
// getter and setter
}
Table_3_lang
#Entity
#Table("table_3_lang")
#IdClass(Table3LangPK.class)
public class Table3Lang {
#Id
#Column(name="id_t3")
private Long idT3;
#Id
#Column(name="id_l")
private Lang l;
#Column(name="name")
private String name;
// getter and setter
}
Table_4
#Entity
#Table("table_4")
public class Table4 {
#Id
#Column(name="id_t4")
private Long idT4;
#Column(name="name")
private String name;
// getter and setter
}
To send data from business layer to front-end i'm using value objects defined as follows:
Simple entity
public class SimpleEntityVO {
private Long entityId;
private String name;
// getter and setter
}
Complex Entity
public class SimpleEntityVO {
private Long entityId;
private SimpleEntityVO tbl2VO;
private SimpleEntityVO tbl3VO;
private SimpleEntityVO tbl4VO;
// ... other field of table_1
// getter and setter
}
In my EJB i need to implement a method that return a list of ComplexEntityVO starting from Table_1
...
private CriteriaBuilder cB = eM.getCriteriaBuilder();
public List<ComplexEntityVO> findAll(Long id_s, Long id_l) {
CriteriaQuery<ComplexEntityVO> cQ = cB.createQuery(ComplexEntityVO.class)
Root<Table1> tbl1Root = cQ.from(Table1.class);
// UPDATE BEGIN
Root<Table2Lang> tbl2Root = cQ.from(Table2Lang.class);
...
Selection<SimpleEntityVO> sESTbl2 = cB.construct(SimpleEntityVO.class, tbl2Root.get(Table2Lang_.id_t2), tbl2Root.get(Table2Lang_.name));
// The selection for table_3_lang and table_4 are the same
// UPDATE END
TypedQuery<ComplexEntityVO> tQ = eM.createQuery(cQ);
}
...
To achieve the results i've tried with join betwen Table1 and Table2Lang, tried with selection like the one exposed below
`Selection<SimpleEntityVO> sES = cB.construct(SimpleEntityVO.class, ...);`
using Root for lang table, tried with solution exposed here
https://community.oracle.com/message/10795956#10795956
but when i try to execute this statement
`cQ.select(cB.construct(ComplexEntityVO.class, id_t1, SimpleEntityVO)`
or this
`cQ.multiselect(...)`
i get the: IllegalArgumentException
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: , near line 1, column 64
[select new com.example.vo.ComplexEntityVO(generatedAlias0.id_t1,
new com.example.labims.vo.SimpleEntityVO(generatedAlias1.table2.id_t2, generatedAlias1.name),
new com.example.vo.SimpleEntityVO(generatedAlias2.table_3.id_t3, generatedAlias2.name),
new com.example.vo.SimpleEntityVO(generatedAlias3.id_t4, generatedAlias3.name),
generatedAlias0.limit, generatedAlias0.value, generatedAlias0.uncertainty)
from com.example.Table1 as generatedAlias0,
com.example.model.Table2Lang as generatedAlias1,
com.example.model.Table3Lang as generatedAlias2,
com.example.model.Table4 as generatedAlias3
where ( generatedAlias0.id_s=:param0 ) and ( ( generatedAlias1.lang.id_l=:param1 ) and ( generatedAlias2.lang.id_l=:param1 ) )]
From the cause of execption understanded that i can't instanciate new object inside select or multiselect statement, but i don't find a way to achieve the original SQL query using criteria API.
UPDATE
i've added an excerpt of what i've tried to achieve the result between //UPDATE BEGIN and //UPDATE END
I think make hibernate show sql == true
and take query by console,test showed query your databases and find error hbernate not generate query correct
There are two approaches to solve this problem.
Add a constructor method to ComplexEntityVO like this:
public ComplexEntityVO(Long id, Long simpleId2, String simpleName2 /* etc ... */) {
this.simpleEntityVo = new SimpleEntityVO(simpleId2, simpleName2);
// etc for other associations
}
add a ProjectionList to your query, return a List<Object[]> instead of a List<ComplexEntityVO> and then iterate over the results like so
for(Object[] o: results) {
ComplexEntityVO cvo = new ComplexEntityVO((Long)o[0]);
new SimpleEntityVO vo2 = new SimpleEntityVO((Long) o[1], (String) o[2]);
cvo.setTbl2VO(vo2);
// ... etc for other associations
}
Although the second is uglier I would prefer it, since it is more flexible, and allows more opportunities for debugging, logging etc.
See AliasToBeanResultTransformer(MyDTO.class) fails to instantiate MyDTO

Best Practices for JPA - joining read-only data

I am using a JPA model with two classes. The first one is mapping a table with "dynamic" data, the second one is mapping a table with read-only, reference data.
As an example, I have a Person entity mapping a Person Table, that contains a #OneToOne reference to the Civility entity, which itself maps to the Civility table (2 columns) that only has 3 records in it (Miss, Mrs and Mr).
I wanted to know the best way to write a query on the person entity based on Civility value. For example, what query would I use to get all Person's with civility = Mr?
Thanks.
one way to map reference lookup data is to use the #Enumerated annotation in jpa. You still have to create enumeration with the lookup values, but that's why it's reference data anyway.
For example, I have a rating code, and its a string/varchar value on table.
But can use a enumeration to use it:
#Enumerated(EnumType.STRING)
#Column
public RatingCode getRating() {
return rating;
}
public void setRating(RatingCode rating) {
this.rating = rating;
}
and the enumeration is:
public enum RatingCode {
Core, Star
}
Use a unit test to try all values, and you know it's a safe way to get reference data.
You can still use HQL to pull out the values, and pass the enumeration as the value:
hql = "select r from Rating as r where r.rating = :aEnum"
// and in the call to pass the parameter
qry.setParameter("aEnum", aRatingCode)
The enumeration is a field within the Rating entity:
#Entity
#Table
public class Rating {
private Integer rating_Id;
private RatingCode rating;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column
public Integer getRating_Id() {
return rating_Id;
}
public void setRating_Id(Integer rating_Id) {
this.rating_Id = rating_Id;
}
#Enumerated(EnumType.STRING)
#Column
public RatingCode getRating() {
return rating;
}
public void setRating(RatingCode rating) {
this.rating = rating;
}
}
So I have a profile, that requires a Rating, so I lookup a rating via the enumeration and add it to the profile.
Profile p = new Profile();
RatingServiceI rs = new RatingService()
Rating r = rs.getRating(RatingCode.Core);
p.setRating(r);
You didn't post your entity definitions, so you will need to interpret the code in this answer to match up with your actual models. Also, note that querying the entities themselves, in this case, has nothing to do whether the data in the underlying tables is 'read-only' or not:
final String queryStr = "SELECT p FROM Person p WHERE p.civility.value = :value";
final TypedQuery<Person> query = entityManager.createQuery(queryStr, Person.class);
query.setParameter("value", "Mr");
List<Person> results = query.getResultList();

Categories

Resources